/**
 * Copyright (C) 2007 The AsyncMail Group <asyncmail@googlegroups.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.asyncmail.smtpclient;

import org.apache.commons.codec.binary.Base64;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.common.TrafficMask;
import org.apache.mina.filter.SSLFilter;
import org.apache.mina.filter.StreamWriteFilter;

import com.googlecode.asyncmail.common.MailAddress;
import com.googlecode.asyncmail.common.MailEnvelope;
import com.googlecode.asyncmail.smtp.SMTPRequest;
import com.googlecode.asyncmail.smtp.SMTPRequestImpl;
import com.googlecode.asyncmail.smtp.SMTPResponse;
import com.googlecode.asyncmail.smtp.SMTPResponseImpl;
import com.googlecode.asyncmail.smtp.SMTPRetCode;

import javax.net.ssl.SSLContext;

import java.io.InputStream;
import java.util.LinkedList;
import java.util.Queue;

public class SMTPClientIoHandler extends IoHandlerAdapter {

    private static final String PIPELINING_SUPPORTED = "PIPELINING_SUPPORTED";

    private static final String SENT_COMMANDS = "SENT_COMMANDS";

    private static final String COMMANDS_QUEUE = "COMMANDS_QUEUE";

    private static final String STARTTLS_SUPPORTED = "STARTTLS_SUPPORTED";

    private SSLContext sslContext = null;

    @Override
    public void exceptionCaught(IoSession arg0, Throwable arg1)
            throws Exception {
        System.err.println("exceptionCaught: " + arg1.getMessage());
        System.err.println("error on local conn: " + arg0.getLocalAddress());
        arg1.printStackTrace();
        System.err.println("command: " + getCommandFromQueue(arg0));
        // super.exceptionCaught(arg0, arg1);
    }

    @SuppressWarnings("unchecked")
    private SMTPRequest getCommandFromQueue(IoSession arg0) {
        Queue<SMTPRequestImpl> commandQueue = (Queue<SMTPRequestImpl>) arg0
                .getAttribute(SENT_COMMANDS);
        if (commandQueue == null) {
            commandQueue = new LinkedList<SMTPRequestImpl>();
            arg0.setAttribute(SENT_COMMANDS, commandQueue);
        }
        SMTPRequest poll = commandQueue.poll();
        return poll;
    }

    @Override
    public void messageReceived(IoSession arg0, Object arg1) throws Exception {
        // System.out.println("messageReceived "+arg1);
        if (arg1 instanceof SMTPResponseImpl) {
            handleCommandResponse(arg0, getCommandFromQueue(arg0),
                    (SMTPResponse) arg1);
        } else {
            System.out.println("WEIRD REPLY: " + arg1.getClass() + " => "
                    + arg1);
            super.messageReceived(arg0, arg1);
        }
    }

    private void handleCommandResponse(IoSession arg0, SMTPRequest request,
            SMTPResponse arg1) {
        // welcome message
        MailEnvelope mail = (MailEnvelope) arg0.getAttribute("MAIL");
        // System.out.println("received reply to "+request+" as "+arg1);
        // TODO when the server welcome message does not start with "220" this
        // throws an NPE!
        if ((request == null && SMTPRetCode.SERVICE_READY.equals(arg1.getRetCode()))
                || ("STARTTLS".equals(request.getCommand()) && SMTPRetCode.SERVICE_READY
                        .equals(arg1.getRetCode()))) {
            if (request != null && "STARTTLS".equals(request.getCommand())) {
                // System.err.println("starting TLS....");
                arg0.setTrafficMask(TrafficMask.WRITE);
                SSLFilter filter = new SSLFilter(sslContext);
                filter.setUseClientMode(true);
                arg0.getFilterChain().addFirst("sslFilter", filter);
                arg0.setTrafficMask(TrafficMask.ALL);
                arg0.setAttribute("STARTTLS_ACTIVATED");

            }
            SMTPRequestImpl newRequest = new SMTPRequestImpl("EHLO", "me.void.it.");
            // should never enqueue, but always write!
            enqueueRequest(arg0, newRequest);
        } else if (("EHLO".equals(request.getCommand()) && SMTPRetCode.MAIL_OK.equals(arg1
                .getRetCode()))
                || SMTPRetCode.AUTH_OK.equals(arg1.getRetCode())) {

            if (SMTPRetCode.AUTH_OK.equals(arg1.getRetCode())) {
                arg0.setAttribute("AUTHENTICATED");
            } else {
                // use AUTH
                if (arg1.getLines().contains("PIPELINING")) {
                    arg0.setAttribute(PIPELINING_SUPPORTED);
                }
                // use STARTTLS
                /*
                 * TODO temporarily disabled if
                 * (arg1.getLines().contains("STARTTLS")) {
                 * arg0.setAttribute(STARTTLS_SUPPORTED); }
                 */
            }

            if (arg0.containsAttribute(STARTTLS_SUPPORTED)
                    && !arg0.containsAttribute("STARTTLS_ACTIVATED")) {
                SMTPRequestImpl request2 = new SMTPRequestImpl("STARTTLS", null);
                enqueueRequest(arg0, request2);
            } else if (arg1.getLines().contains("AUTH LOGIN PLAIN")
                    && !arg0.containsAttribute("AUTHENTICATED")) {
                // try the AUTH login
                SMTPRequestImpl request2 = new SMTPRequestImpl("AUTH", "PLAIN");
                // +new String(Base64.encodeBase64("marissa".getBytes()))
                enqueueRequest(arg0, request2);
            } else {
                SMTPRequestImpl request2;
                MailAddress from = mail.getSender();
                if (from == null) {
                    request2 = new SMTPRequestImpl("MAIL", "FROM:<>");
                } else {
                    request2 = new SMTPRequestImpl("MAIL", "FROM:<"
                            + mail.getSender().getAddressAsString() + ">");
                }
                enqueueRequest(arg0, request2);
                for (MailAddress addr : mail.getRecipients()) {
                    SMTPRequestImpl request3 = new SMTPRequestImpl("RCPT", "TO:<"
                            + addr.getAddressAsString() + ">");
                    enqueueRequest(arg0, request3);
                }
                SMTPRequestImpl request3 = new SMTPRequestImpl("DATA", null);
                enqueueRequest(arg0, request3);
            }
        } else if ("AUTH".equals(request.getCommand())) {
            // System.err.println(arg1);
            if (SMTPRetCode.AUTH_READY.equals(arg1.getRetCode())) {
                SMTPRequestImpl newRequest = new SMTPRequestImpl(new String(Base64
                        .encodeBase64("\0marissa\0koala".getBytes())), null);
                enqueueRequest(arg0, newRequest);
            }
        } else if ("RCPT".equals(request.getCommand())
                || "MAIL".equals(request.getCommand())) {
            // empty the queue
            checkQueue(arg0);
        } else if ("DATA".equals(request.getCommand())) {
            if (SMTPRetCode.DATA_READY.equals(arg1.getRetCode())) {
                arg0.setTrafficMask(TrafficMask.WRITE);
                arg0.getFilterChain().addLast("streamFilter",
                        new StreamWriteFilter());
                arg0.write(mail.getBodyInputStream());
            }
        } else if (".".equals(request.getCommand())) {
            SMTPRequestImpl request2 = new SMTPRequestImpl("QUIT", null);
            enqueueRequest(arg0, request2);
        } else if ("QUIT".equals(request.getCommand())) {
            if (SMTPRetCode.SYSTEM_QUIT.equals(arg1.getRetCode())) {
                // CloseFuture future =
                arg0.close();
                // System.out.println("CLOSING!!");
            } else {
                System.out.println("Bad result on close!");
            }
        } else {
            System.out.println("WHAT? " + request + " | " + arg1);
        }
    }

    /**
     * @param arg0
     * @param request3
     */
    @SuppressWarnings("unchecked")
    private void enqueueRequest(IoSession arg0, SMTPRequestImpl request3) {
        Queue<SMTPRequestImpl> linkedList = ((Queue<SMTPRequestImpl>) arg0
                .getAttribute(SENT_COMMANDS));
        if (linkedList.size() > 0
                && !arg0.containsAttribute(PIPELINING_SUPPORTED)) {
            System.out
                    .print("#### PIPELINING NOT SUPPORTED, ENQUEUE NEW COMMAND");
            Queue<SMTPRequestImpl> queue = (Queue<SMTPRequestImpl>) arg0
                    .getAttribute(COMMANDS_QUEUE);
            if (queue == null) {
                queue = new LinkedList<SMTPRequestImpl>();
                arg0.setAttribute(COMMANDS_QUEUE, queue);
            }
            System.out.println("COMMAND ENQUEUED");
            queue.add(request3);
        } else {
            linkedList.add(request3);
            arg0.write(request3);
        }
    }

    /**
     * @param arg0
     */
    @SuppressWarnings("unchecked")
    private void checkQueue(IoSession arg0) {
        Queue<SMTPRequestImpl> queue = (Queue<SMTPRequestImpl>) arg0
                .getAttribute(COMMANDS_QUEUE);
        if (queue != null) {
            SMTPRequestImpl req = queue.poll();
            enqueueRequest(arg0, req);
        }
    }

    @Override
    public void messageSent(IoSession arg0, Object arg1) throws Exception {
        if (arg1 instanceof InputStream) {
            ((InputStream) arg1).close();
            arg0.getFilterChain().remove("streamFilter");
            arg0.setTrafficMask(TrafficMask.ALL);
            SMTPRequestImpl request = new SMTPRequestImpl(".", null);
            enqueueRequest(arg0, request);
        } else {
            // System.out.println("messageSent");
            super.messageSent(arg0, arg1);
        }
    }

    @Override
    public void sessionClosed(IoSession arg0) throws Exception {
        // System.out.println("messageClosed");
        super.sessionClosed(arg0);
    }

    @Override
    public void sessionCreated(IoSession arg0) throws Exception {
        // disable traffic until we receive the notification of connection
        // established
        arg0.setTrafficMask(TrafficMask.NONE);
        // DO not call super.sessionCreated because it calls the setKeepAlive
        // and
        // throw an exception on Vista.
        // super.sessionCreated(arg0);
    }

    @Override
    public void sessionIdle(IoSession arg0, IdleStatus arg1) throws Exception {
        System.out.println("sessionIdle");
        super.sessionIdle(arg0, arg1);
    }

    @Override
    public void sessionOpened(IoSession arg0) throws Exception {
        // System.err.println("sessionOpened: "+System.identityHashCode(arg0));
        super.sessionOpened(arg0);
    }

    public void setSSLContext(SSLContext context) {
        this.sslContext = context;
    }
}